关于单元测试
单元测试的优点
- 单元测试是指对软件中的最小可测试单元进行检查和验证。
- 在Java中的单元测试,一般是对一个类的测试。能让coder极为迅速并且准确的定位错误的来源。因此,极大的减少了我们调试的时间。
关于隔离测试(Mock)和Mock对象
- 在一个大项目或者关系比较紧密的项目中,很有可能出现两个子系统之间的接口依赖,因此极可能会造成前期开发中基础系统一直在开发接口,而自己的功能只能放后。
- 隔离测试它使得我们可以测试还未写完的代码(只要你有接口可使用),另外,隔离测试能帮助团队单元测试代码的一部分,而无需等待全部代码的完成。
如图:
从上图可以看出,如果我们要对A进行测试,那么就要先把整个依赖树构建出来,也就是BCDE的实例。
而一种替代方案就是使用Mock:
- mock对象就是在调试期间用来作为真实对象的替代品。
- mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试。
- Mockito就是一种mock框架。
关于测试替身
- Stub(桩)-用简单可能的实现来代替真实的实现。
- Fake(伪造对象)-优化的伪造真实事物的行为。
- SPY(测试间谍)-用于记录发生的情况, 用于事后验证。
Mock(模拟对象)-特定情景下可配置行为的对象。
Mock和Stub的异同:
- 相同点:Stub和Mock对象都是用来模拟外部依赖,使我们能控制。
- 不同点:而stub完全是模拟一个外部依赖,用来提供测试时所需要的测试数据。而mock对象用来判断测试是否能通过,也就是用来验证测试中依赖对象间的交互能否达到预期。
- 在mocking框架中mock对象可以同时作为stub和mock对象使用,两者并没有严格区别。
Mockito
什么是Mockito
- Mockito是mocking框架,是Google Code上的一个开源项目,Api相对于EasyMock更好友好。Mockito简单易学,它可读性强和验证语法简洁。
- 用Mockito完成单元测试的过程大致可以划分为以下几个步骤:
- 生成 Mock 对象。
- 设定 Mock 对象的预期行为和输出。
- 调用 Mock 对象方法进行单元测试。
- 对 Mock 对象的行为进行验证。
IDEA里新建某个类的测试类的方法
- 需要下载Junit插件。
- maven导入相关junit和mockito依赖包。
- 建立或跳转到当前类的测试类:Ctrl+Shift+T。
- 编辑测试和设置运行:
- run-edit configuration 可以设置运行的测试类。
- 下载junit插件后,类前有运行箭头,可选择run/debug/with coverage,最后一个是显示测试覆盖率。会在跑完后在被测试的类前显示颜色以标识覆盖情况(覆盖到的绿色,没覆盖到红色)。
Mocktio实例
以某段测试类代码为例:
1 | //使用built-in runner:MockitoJUnitRunner初始化mock的代码 |
几个常用注解
- @RunWith(MockitoJUnitRunner.class):使用Mockito一般需要在类前加上这个注解。
- @Mock:创建模拟的对象。
- 注:不能mock静态、final、私有方法等。
- @InjectMocks:被注入mock对象的被测试类。
@Test:把一个方法标记为测试方法。Test的属性有:
except:用来测试异常,如图,如果这个测试方法抛出了异常则通过,如果没抛出异常 则测试不通过执行fail(“factorial参数为负数没有抛出异常”)。
timeout:测试一个方法能否在规定时间完成:
如@Test(timeout=2000):测试该测试方法是否在2000毫秒内完成。
- @Before:每一个测试方法执行前自动调用一次。
@After:每一个测试方法执行完自动调用一次。
@BeforeClass:所有测试方法执行前执行一次,在测试类还没有实例化就已经被加载,所以用static修饰。
- @AfterClass:所有测试方法执行完执行一次,在测试类还没有实例化就已经被加载,所以用static修饰。
@Ignore:暂不执行该测试方法。
执行顺序:BeforeClass→(构造方法)→Before→Test1→After→(构造方法)→Before→Test2→After→AfterClas。
- @BeforeClass和@AfterClass在类被实例化前(构造方法执行前)就被调用了,而且只执行一次,通常用来初始化和关闭资源。
- @Before和@After和在每个@Test执行前后都会被执行一次。
- @Test标记一个方法为测试方法,被@Ignore标记的测试方法不会被执行,例如这个模块还没完成或者现在想测试别的不想测试这一块。
- JUnit4为了保证每个测试方法都是单元测试,是独立的互不影响。所以每个测试方法执行前都会重新实例化测试类(构造方法多次调用)。
常用语法
设定 Mock 对象某个方法调用时的返回值
@Mock设置生成了mock对象,那么这是用Mockito完成单元测试的第二步:设定Mock对象的预期行为和输出。
有两种书写方式:
when(mock.someMethod()).thenReturn(value)
或者
doReturn(value).when(mock).someMethod()
验证被测试方法
即用Mockito完成单元测试的第四步:对Mock对象的行为进行验证。
Mock 对象一旦建立便会自动记录自己的交互行为,所以我们可以有选择的对它的 交互行为进行验证在 Mockito中验证 Mock 对象交互行为的方法是:
verify(mock).someMethod(…)。
可以验证调用次数,如:
verify(mock, times(num)).someMethod(…)。
1 |
|
对方法设定返回异常
when(list.get(1)).thenThrow(new RuntimeException(“test excpetion”));
或者
doThrow(new RuntimeException(“test excpetion”)).when(list).get(1);
doThrow在运行测试方法时如果报错会抛出相应异常,从而覆盖被测试方法中的catch部分。如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private BatchTransferAcctImportComp transferAcctImportComp;
ExceptionChecker exceptionChecker;
private AcctService acctService;
private Acct acct;
public void buildWholesaleInstExWithWrongFormat() throws BaseAppException {
Mockito.doThrow(new BaseAppException(OrderErrorCodeDef.CVBS_ORDER_SUBS_CUST_IS_DIFFERENT)).when(exceptionChecker)
.publishBizException(OrderErrorCodeDef.CVBS_ORDER_SUBS_CUST_IS_DIFFERENT);
//创建用于测试的对象
WholesaleInstExDto wholesaleInstExDto = new WholesaleInstExDto();
wholesaleInstExDto.setAcctId("1");
wholesaleInstExDto.setCustId("1");
Map<String, String> paramStrMap = Maps.newHashMap();
paramStrMap.put("A", "1000");
//设置mock对象预期行为和输出
Mockito.when(acctService.queryAcctByAcctNbr("100")).thenReturn(acct);
Mockito.when(acct.getAcctId()).thenReturn("1");
Mockito.when(acct.getCustId()).thenReturn("2");
//调用被测试方法,这个方法校验wholesaleInstExDto和paramStrMap的CustId须一致。因此按照设定值,此处会抛出异常。
transferAcctImportComp.buildWholesaleInstEx(wholesaleInstExDto, paramStrMap);
}
也可以使用org.junit.rules.ExpectedException规则:
标准的JUnit的org.junit.Test注解提供了一个expected属性,你可以用它来指定一个Throwble类型,如果方法调用中抛出了这个异常,这条测试用例就算通过了。很多情况下有它就足够了,不过如果你想验证下异常的信息——你就得另寻出路了。使用ExpectedException来实现这个非常简单:
1
2
3
4
5
6
7
8
9
10public class ExpectedExceptionsTest {
public ExpectedException thrown = ExpectedException.none();
public void verifiesTypeAndMessage() {
thrown.expect(RuntimeException.class);
thrown.expectMessage("Runtime exception occurred");
throw new RuntimeException("Runtime exception occurred");
}
}在这段代码中,我们可以期望抛出的异常中包含指定的信息。
参数匹配器
- 如 anyInt、anyString、anyMap…..
- 需要注意的是:如果使用参数匹配器,那么所有的参数都要使用参数匹配器,不管是stubbing还是verify的时候都一样。
如:1
2
3
4
5
6
7
public void argumentMatcherTest2(){
Map<Integer,String> map = mock(Map.class);
when(map.put(anyInt(),anyString())).thenReturn("hello");//anyString()替换成"hello"就会报错
map.put(1, "world");
verify(map).put(eq(1), eq("world")); //eq("world")替换成"world"也会报错
}
断言
即验证测试结果与期望值是否一致。
一些常用断言:
- assertEquals
比较实际的值和用户预期的值是否一样。 - assertTrue、与assertFalse
判断某个条件是真还是假,如果和预期的值相同则测试成功,否则将失败。 - assertNull与assertNotNull
验证所测试的对象是否为空或不为空。 - assertSame与assertNotSame
测试预期的值和实际的值是否为同一个参数(即判断是否为相同的引用)。assertNotSame则测试预期的值和实际的值是不为同一个参数。 - fail
fail断言能使测试立即失败,这种断言通常用于标记某个不应该被到达的分支。
to be continued